package com.yanyeori.framework.security.service;

import com.yanyeori.framework.core.constant.ChannelEnum;
import com.yanyeori.framework.core.context.UserContext;
import com.yanyeori.framework.core.model.WebResponse;
import com.yanyeori.framework.core.util.HttpUtil;
import com.yanyeori.framework.core.util.IdUtil;
import com.yanyeori.framework.core.util.StringUtil;
import com.yanyeori.framework.security.bo.UserDetail;
import com.yanyeori.framework.security.cache.PermissionsCache;
import com.yanyeori.framework.security.config.ConfigFactory;
import com.yanyeori.framework.security.constant.SecurityConst;
import com.yanyeori.framework.security.constant.SsoErrorCode;
import com.yanyeori.framework.security.database.UserDao;
import com.yanyeori.framework.security.event.RemoteLogoutEvent;
import com.yanyeori.framework.security.exception.MyUsernameNotFoundException;
import com.yanyeori.framework.security.exception.PwdErrorTimesOverException;
import com.yanyeori.framework.security.util.SecurityUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.event.EventListener;
import org.springframework.data.redis.core.BoundListOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.scheduling.annotation.Async;
import org.springframework.security.authentication.AccountStatusException;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;
import java.util.concurrent.TimeUnit;

/**
 * token登录服务
 *
 * @author chenkuan 2020/11/29
 */
@Slf4j
@Component
public class TokenLoginService {

    @Autowired
    private PermissionsService permissionsService;
    @Autowired
    private UserDao userDao;

    private final AuthenticationManager authenticationManager;

    public TokenLoginService(AuthenticationManager authenticationManager) {
        this.authenticationManager = authenticationManager;
    }

    private static RedisTemplate<String, Object> redisTemplate() {
        return ConfigFactory.getRedisTemplate();
    }

    private static ValueOperations<String, Object> opsForValue() {
        return ConfigFactory.getRedisTemplate().opsForValue();
    }

    private static BoundListOperations<String, Object> boundListOps(String key) {
        return ConfigFactory.getRedisTemplate().boundListOps(key);
    }

    /**
     * redis token key
     */
    public static String redisTokenKey(String token) {
        return StringUtil.format(SecurityConst.REDIS_TOKEN_KEY, token);
    }

    /**
     * redis 在线数据 key
     */
    public static String redisOnlineKey(String userId, ChannelEnum channel) {
        return StringUtil.format(SecurityConst.REDIS_ONLINE_KEY, userId, channel);
    }

    /**
     * 用户登录创建token
     *
     * @param userDetail 用户信息
     * @return token
     */
    public String generateToken(UserDetail userDetail) {
        String token = IdUtil.randomUUID();
        opsForValue().set(redisTokenKey(token), userDetail, getTokenExpiration(userDetail.getChannel()), TimeUnit.MINUTES);
        return token;
    }

    /**
     * 处理用户密码登录
     */
    public WebResponse<Authentication> login(HttpServletRequest request, String username, String password) {
        try {
            username = UsernamePasswordDecrypt.get().username(request, username);
            password = UsernamePasswordDecrypt.get().password(request, password);
        } catch (Exception e) {
            log.error("Decrypt username or password fail", e);
            return WebResponse.fail(SsoErrorCode.SSO_1019);
        }
        //用户验证
        Authentication authentication;
        try {
            authentication = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, password));
        } catch (MyUsernameNotFoundException e) {
            //用户名不存在
            return WebResponse.fail(SsoErrorCode.SSO_1000);
        } catch (PwdErrorTimesOverException e) {
            //当日密码输入错误次数已超出{0}次，请次日再试
            return WebResponse.fail(SsoErrorCode.SSO_1001, ConfigFactory.getConfig().getDayPwdErrorTimes());
        } catch (AccountStatusException e) {
            log.warn("User status exception");
            return WebResponse.fail(SsoErrorCode.SSO_1002);
        } catch (AuthenticationException e) {
            log.warn("Username or password error");
            //登录账号密码认证失败处理
            loginErrorHandle(username);
            Integer surPwdTimes = getSurPwdTimes(username);
            return WebResponse.fail(surPwdTimes == null ? SsoErrorCode.SSO_1003 : SsoErrorCode.SSO_1003_1, surPwdTimes);
        } catch (Exception e) {
            log.error("An error occurred while logging in. Please try again later", e);
            return WebResponse.fail(SsoErrorCode.SSO_1004);
        }
        //存储认证信息
        UserDetail userDetail = (UserDetail) authentication.getPrincipal();
        userDetail.setPassword(null);
        userDetail.setChannel(SecurityUtil.getChannel(request));
        userDetail.setLoginIp(HttpUtil.getClientIP(request));
        SecurityContextHolder.getContext().setAuthentication(authentication);
        //保存用户上下文信息
        SecurityUtil.saveUserContext(userDetail);
        log.debug("Save user context info: UserContext");
        //初始化用户权限数据
        initUserPermissions();
        return WebResponse.ok(authentication);
    }

    /**
     * 初始化用户权限数据
     */
    public void initUserPermissions() {
        UserContext userContext = UserContext.getRequired();
        if (ConfigFactory.getConfig().isEnableInterfacePerAuth() && !ConfigFactory.getConfig().isDevMode()) {
            PermissionsCache.PERMISSIONS_MAP.putAll(permissionsService.loadInterfacePermissionsByUser(userContext.getUserId()));
        }
        log.debug("Loading user [{}] permission data completed", userContext.getUsername());
    }

    /**
     * 远程登出事件监听
     * 异步登出用户信息，清除用户权限数据
     *
     * @param event RemoteLogoutEvent
     */
    @Async
    @EventListener
    public void listenRemoteLogoutEvent(RemoteLogoutEvent event) {
        //清除用户权限数据
        PermissionsCache.PERMISSIONS_MAP.remove(event.getUserId());
        log.debug("Async cleanup user [{}] permission data completed", event.getUsername());
    }

    /**
     * 设备在线维护，超出踢人
     *
     * @param token token
     */
    public void deviceOnline(String token) {
        UserContext userContext = UserContext.getRequired();
        if (!isDeviceOnlineLimit(userContext.getChannel())) return;
        int deviceLimit = getDeviceLimit(userContext.getChannel());
        BoundListOperations<String, Object> listOperations = boundListOps(redisOnlineKey(userContext.getUserId(), userContext.getChannel()));
        Long size = listOperations.size();
        if (size != null && size >= deviceLimit) {
            OnlineInfo onlineInfo = (OnlineInfo) listOperations.leftPop();
            if (onlineInfo != null && Boolean.TRUE.equals(redisTemplate().delete(redisTokenKey(onlineInfo.token)))) {
                log.info("Device login restriction, logout userId: {}, ip: {}", userContext.getUserId(), onlineInfo.loginIp);
            }
        }
        listOperations.rightPush(new OnlineInfo(userContext.getLoginIp(), token));
        listOperations.expire(getTokenExpiration(userContext.getChannel()), TimeUnit.MINUTES);
    }

    /**
     * 通过token获取登录用户，返回null则未登录
     *
     * @param token token
     * @return UserDetail
     */
    public UserDetail getUserByToken(String token) {
        return (UserDetail) opsForValue().get(redisTokenKey(token));
    }

    /**
     * token续期
     *
     * @param token      token
     * @param userDetail 用户信息
     */
    public void tokenRenewal(String token, UserDetail userDetail) {
        long tokenExpiration = getTokenExpiration(userDetail.getChannel());
        Boolean expire = redisTemplate().expire(redisTokenKey(token), tokenExpiration, TimeUnit.MINUTES);
        if (Boolean.TRUE.equals(expire) && isDeviceOnlineLimit(userDetail.getChannel())) {
            BoundListOperations<String, Object> listOperations = boundListOps(redisOnlineKey(userDetail.getUserId(), userDetail.getChannel()));
            listOperations.expire(tokenExpiration, TimeUnit.MINUTES);
        }
    }

    /**
     * 移除登录token
     *
     * @param token      token
     */
    public void removeToken(String token) {
        UserContext userContext = UserContext.getRequired();
        Boolean delete = redisTemplate().delete(redisTokenKey(token));
        if (Boolean.TRUE.equals(delete) && isDeviceOnlineLimit(userContext.getChannel())) {
            BoundListOperations<String, Object> listOperations = boundListOps(redisOnlineKey(userContext.getUserId(), userContext.getChannel()));
            listOperations.remove(1, new OnlineInfo(userContext.getLoginIp(), token));
        }
    }

    /**
     * 登录账号密码认证失败处理
     */
    private void loginErrorHandle(String username) {
        //当日密码错误次数+1
        userDao.pwdErrorTimeIncr(username);
    }

    /**
     * 获取账号剩余的密码错误次数
     */
    private Integer getSurPwdTimes(String username) {
        if (ConfigFactory.getConfig().getDayPwdErrorTimes() <= 0) {
            return null;
        }
        UserDetail userDetail = userDao.getByUsername(username);
        int surTimes = ConfigFactory.getConfig().getDayPwdErrorTimes() - userDetail.getPwdErrorTimes();
        return Math.max(surTimes, 0);
    }

    private long getTokenExpiration(ChannelEnum channel) {
        return ConfigFactory.getConfig().getToken().getTokenExpiration().getOrDefault(channel, SecurityConst.TOKEN_DEFAULT_EXPIRATION);
    }

    private boolean isDeviceOnlineLimit(ChannelEnum channel) {
        return ConfigFactory.getConfig().getOnline().isEnableDeviceLimit() && getDeviceLimit(channel) > 0;
    }

    private int getDeviceLimit(ChannelEnum channel) {
        return ConfigFactory.getConfig().getOnline().getDeviceNumMap().getOrDefault(channel, 0);
    }

    private static class OnlineInfo {
        String loginIp;
        String token;

        public OnlineInfo() {
        }

        public OnlineInfo(String loginIp, String token) {
            this.loginIp = loginIp;
            this.token = token;
        }
    }
}
